Перейти к основному содержимому

2.05. Скрипты в Unix

Разработчику Архитектору Инженеру

Скрипты в Unix

Скрипты — это текстовые программы, написанные для оболочки Unix-системы. Они позволяют собирать последовательности команд в один файл и выполнять их как единое действие. Их главная функция — автоматизировать повторяющиеся, рутинные или сложные операции, которые вручную требуют времени, внимания и подвержены ошибкам.

Оболочка Unix (Shell) — это не просто интерфейс для ввода команд. Она представляет собой полноценный язык программирования. Любая команда, которую пользователь вводит в терминале, может быть помещена в файл, дополнена логикой, условиями, циклами и повторным использованием — и превратиться в скрипт. Это делает оболочку мощным инструментом не только для интерактивного управления системой, но и для создания программ без компиляции.

Скрипты не требуют установки дополнительных сред разработки или компиляторов. Они работают в любой Unix-подобной системе — Linux, macOS, BSD — сразу после установки. Даже минимальные дистрибутивы содержат Bash или другую совместимую оболочку. Это делает скрипты универсальным средством для системных задач, быстрых исправлений, развёртывания окружений и подготовки данных.

Оболочка — интерпретатор скриптов

Оболочка — это программа, которая читает команды, интерпретирует их и передаёт ядру операционной системы для выполнения. Самые распространённые оболочки — Bash, Zsh, Ksh, Dash. Bash — стандарт для большинства дистрибутивов Linux и долгое время был основной оболочкой macOS (сменённой на Zsh с 2019 года). Zsh отличается расширенными возможностями автодополнения, подсветки синтаксиса и настройки. Fish — дружелюбна к новичкам, предлагает подсказки прямо во время набора и не требует глубокого знания исторических соглашений.

Каждая оболочка имеет собственный синтаксис для управления потоком выполнения, работы с переменными, обработки строк. Например, условные конструкции if, циклы for и while, операторы case — это встроенные функции самой оболочки, а не внешние программы. Они обрабатываются внутри процесса интерпретатора и не порождают отдельные исполняемые файлы.

Чтобы узнать, какая оболочка используется в текущем сеансе, достаточно выполнить команду:

echo $SHELL

Эта команда показывает путь к оболочке, назначенной пользователю по умолчанию в системе.

Точную информацию о пользователе, включая домашнюю директорию и назначенную оболочку, можно получить из системного файла /etc/passwd:

grep $USER /etc/passwd

Вывод содержит запись вида:

timur:x:1000:1000:Timur Tagirov:/home/timur:/bin/bash

Последнее поле — путь к исполняемому файлу оболочки.

Список всех оболочек, допустимых для входа в систему, хранится в файле /etc/shells. Команды, перечисленные в этом файле, считаются безопасными и могут использоваться как логин-шеллы. Просмотреть его содержимое можно так:

cat /etc/shells

В типичной современной системе там могут присутствовать /bin/sh, /bin/bash, /bin/zsh, /usr/bin/fish, а также специализированные оболочки вроде screen или tmux, которые управляют сессиями терминала.

Выбор оболочки для написания скрипта — важное решение. Лучшая практика — использовать ту же оболочку, с которой пользователь работает в терминале. Это гарантирует предсказуемое поведение, совместимость с уже известными командами и снижает риск ошибок, вызванных различиями в синтаксисе.

Первая строка — указатель интерпретатора

Каждый скрипт начинается со строки, называемой shebang — сокращение от «sharp bang», то есть символы # и !. Эта строка указывает системе, какой интерпретатор использовать для выполнения содержимого файла.

Пример shebang для Bash:

#!/bin/bash

Для Zsh:

#!/usr/bin/zsh

Для совместимости с минимальными системами иногда используется /bin/sh, хотя на практике /bin/sh часто является символической ссылкой на более лёгкий интерпретатор, например Dash, который поддерживает не весь набор расширений Bash.

Если shebang отсутствует, скрипт выполняется в текущей оболочке — той, из которой он был запущен. Это может привести к неожиданному поведению, особенно если скрипт использует специфичные для Bash конструкции ([[ ]], ассоциативные массивы), а вызывающая оболочка — например, dash или sh. Явное указание интерпретатора делает скрипт самодостаточным и переносимым.

Создание и запуск скрипта

Скрипт — это обычный текстовый файл. Его можно создать в любом редакторе: nano, vim, gedit, VS Code — выбор зависит от предпочтений пользователя. Имя файла может быть произвольным, но распространено использование расширения .sh, например backup.sh или update-system.sh. Расширение не обязательно для работы, но помогает визуально идентифицировать назначение файла.

Простейший скрипт может содержать всего две строки:

#!/bin/bash
date +"%d %B %Y"

Первая строка — shebang, вторая — команда date с форматом вывода: день, полное название месяца, год. Сохраним этот файл как today.sh.

Чтобы запустить скрипт, нужно два условия:

  1. Файл должен быть исполняемым.
    В Unix права доступа управляются отдельно для чтения, записи и исполнения. Команда chmod изменяет эти права. Чтобы дать владельцу право на запуск, используют:

    chmod u+x today.sh

    Чтобы разрешить запуск всем пользователям:

    chmod a+x today.sh

    Права 755 означают: владелец — чтение, запись, исполнение (7); группа и остальные — чтение и исполнение (5). Такие права подходят для большинства общедоступных скриптов.

  2. Файл должен быть вызван корректно.
    Если скрипт находится в текущей директории, его запускают с префиксом ./:

    ./today.sh

    Без ./ система ищет команду в путях, перечисленных в переменной PATH, и не найдёт локальный файл.

Если скрипт ещё не сделан исполняемым, его можно запустить через явный вызов интерпретатора:

bash today.sh

Или — как внутреннюю команду оболочки — через source или сокращённый синтаксис с точкой:

. today.sh

Этот способ выполняет команды скрипта в текущем процессе оболочки, а не в дочернем. Это важно, если скрипт изменяет переменные окружения или текущую директорию — такие изменения останутся в силе после завершения.


Переменные — именованные контейнеры для данных

Переменная в Unix-скрипте — это именованная область памяти, в которую можно записать строку и позже извлечь её значение. Присвоение значения переменной происходит без пробелов вокруг знака =:

name="Timur"
count=42
path="/home/user/documents"

Оболочка не различает типы данных: всё — строки. Числовые операции возможны только через специальные конструкции, такие как арифметическое расширение $(( )), но сама переменная хранит текст.

Чтобы обратиться к значению переменной, перед её именем ставится символ $:

echo $name
echo "Всего файлов: $count"

Важно использовать кавычки при подстановке, особенно если значение может содержать пробелы. Без кавычек оболочка разобьёт строку на отдельные слова и передаст их как разные аргументы:

filename="my report.pdf"
ls $filename # ошибка: ls получит два аргумента — "my" и "report.pdf"
ls "$filename" # правильно: один аргумент — "my report.pdf"

Существуют специальные переменные, устанавливаемые самой оболочкой. Например:

  • $0 — имя запущенного скрипта (путь, по которому он был вызван);
  • $1, $2, $3, … — аргументы командной строки;
  • $# — количество переданных аргументов;
  • $? — код возврата последней выполненной команды;
  • $$ — идентификатор текущего процесса (PID);
  • $USER, $HOME, $PATH — переменные окружения, описывающие пользователя и среду.

Эти переменные позволяют скрипту адаптироваться к контексту выполнения: знать, кто его запустил, с какими параметрами, где находится, и как завершилась предыдущая операция.


Аргументы командной строки — входные данные для скрипта

Скрипт становится гибким, когда может принимать входные данные. Самый естественный способ — передача аргументов при запуске:

./process.sh /data/input.csv /output/results.txt

Внутри скрипта первый аргумент доступен как $1, второй — как $2. Их можно сразу использовать:

#!/bin/bash
input_file=$1
output_file=$2

echo "Чтение из: $input_file"
echo "Запись в: $output_file"

Хорошая практика — присваивать аргументы смысловым переменным. Это повышает читаемость и позволяет повторно использовать значение без риска ошибки при пересчёте позиций.

Перед использованием аргументов необходимо проверить их наличие. Если пользователь запустит скрипт без параметров, $1 окажется пустой строкой, и команды, ожидающие путь к файлу, могут повести себя непредсказуемо.

Проверка количества аргументов делается через переменную $#:

#!/bin/bash
if [ $# -lt 2 ]; then
echo "Использование: $0 <входной_файл> <выходной_файл>"
exit 1
fi

Здесь exit 1 завершает скрипт с кодом ошибки. Код 0 означает успешное завершение, любое другое значение — сбой. Программы и другие скрипты могут проверять этот код и реагировать на него.

Можно проверять тип аргументов. Например, убедиться, что первый аргумент — число:

if [[ $1 =~ ^[0-9]+$ ]]; then
loops=$1
else
echo "Ошибка: '$1' не является целым числом"
exit 1
fi

Конструкция [[ ]] — это расширенный условный оператор Bash, поддерживающий регулярные выражения через =~. Он надёжнее, чем классический [ ], и менее подвержен ошибкам интерпретации.


Интерактивный ввод — диалог со скриптом

Не все данные известны заранее. Иногда скрипт должен запросить информацию у пользователя во время выполнения. Для этого используется команда read.

Простейший пример:

echo -n "Введите ваше имя: "
read name
echo "Привет, $name!"

Флаг -n у echo убирает перевод строки, и ввод происходит в той же строке — как в обычных приглашениях.

Команда read может считывать несколько значений за один вызов:

echo "Введите имя и возраст через пробел:"
read name age
echo "$name, вам $age лет."

Если ввод содержит больше слов, чем переменных, остаток присваивается последней переменной.

Можно установить таймаут ожидания ввода:

read -t 10 -p "Введите пароль (у вас 10 секунд): " password

Флаг -p позволяет совместить приглашение и ввод в одной команде. Если пользователь не успел ввести данные, read завершится с ненулевым кодом, и скрипт может обработать это как ошибку.

Для ввода конфиденциальной информации — например, паролей — используется флаг -s, скрывающий символы на экране:

read -s -p "Пароль: " pwd
echo # переход на новую строку после скрытого ввода

Такие приёмы делают скрипты удобными для интерактивного использования — например, в установочных программах, мастерах настройки или диагностических утилитах.


Условия и ветвление — принятие решений

Скрипт, выполняющий одни и те же действия независимо от обстоятельств, ограничен в применении. Настоящая мощь раскрывается, когда он может анализировать состояние и выбирать дальнейшие действия.

Основной инструмент — конструкция if. Её синтаксис:

if условие; then
команды при истине
elif другое_условие; then
команды при второй истине
else
команды при лжи всех условий
fi

Условие — это команда, чей код возврата интерпретируется как логическое значение: 0 — истина, ненулевое — ложь.

Часто условие — это проверка через встроенную команду [ (она же test):

if [ "$day" = "Friday" ]; then
echo "TGIF!"
fi

Обратите внимание: внутри [ ] требуется пробел после открывающей скобки и перед закрывающей. Строки заключаются в кавычки, чтобы избежать ошибок при пустых или многословных значениях. Оператор = проверяет равенство строк.

Для числовых сравнений используются другие операторы:

  • -eq — равно
  • -ne — не равно
  • -lt — меньше
  • -le — меньше или равно
  • -gt — больше
  • -ge — больше или равно

Пример:

if [ $count -gt 100 ]; then
echo "Слишком много элементов"
fi

В Bash предпочтительнее использовать [[ ]], так как он:

  • не требует кавычек в большинстве случаев (автоматически защищает от разбиения слов),
  • поддерживает логические операторы &&, ||, ! внутри условия,
  • позволяет использовать шаблоны (== *.txt) и регулярные выражения (=~).

Пример расширенной проверки:

if [[ -f "$file" && -r "$file" ]]; then
echo "Файл существует и доступен для чтения"
fi

Операторы -f и -r — это проверки:

  • -f — является ли аргумент обычным файлом,
  • -r — доступен ли файл для чтения текущему пользователю.

Другие полезные проверки:

  • -d — директория,
  • -e — файл существует (любой тип),
  • -s — файл существует и не пуст,
  • -x — файл исполняем,
  • -z — строка пустая,
  • -n — строка не пустая.

Эти проверки позволяют строить надёжные сценарии, которые не ломаются при отсутствии файлов, недостатке прав или некорректных входных данных.


Циклы — повторение действий

Циклы позволяют выполнять блок команд многократно. В Unix-скриптах чаще всего используются for и while.

Цикл for: перебор списка

Классический for в Bash перебирает последовательность слов:

for day in Пн Вт Ср Чт Пт Сб Вс; do
echo "День: $day"
done

Список может быть задан явно, как выше, или получен из команды:

for file in *.log; do
echo "Обработка: $file"
gzip "$file"
done

Осторожно: если шаблон *.log не совпадает ни с одним файлом, переменная file примет значение *.log как строку. Чтобы избежать этого, можно включить опцию nullglob:

shopt -s nullglob
for file in *.log; do
# если логов нет — цикл просто не выполнится
process "$file"
done

Можно генерировать числовые последовательности:

for i in {1..10}; do
echo "Шаг $i"
done

Или использовать C-подобный синтаксис:

for (( i=1; i<=10; i++ )); do
echo "Шаг $i"
done

Цикл while: выполнение до тех пор, пока условие истинно

while проверяет условие перед каждой итерацией. Он удобен, когда количество повторов заранее неизвестно.

Пример: ожидание запуска службы:

while ! systemctl is-active --quiet nginx; do
echo "Nginx ещё не запущен... ждём 5 секунд"
sleep 5
done
echo "Nginx запущен"

Здесь ! инвертирует код возврата команды — цикл продолжается, пока systemctl возвращает ненулевой код.

Числовой цикл через while:

n=1
while [ $n -le 5 ]; do
echo "Попытка $n"
((n++))
done

Оператор (( )) — это арифметическое расширение. Внутри него переменные можно использовать без $, и поддерживаются все стандартные математические операции.

Команда break прерывает цикл, continue — переходит к следующей итерации.


Оператор case — выбор по шаблону

Когда скрипт должен выбрать одно из нескольких возможных действий, основываясь на значении переменной, множественные ветки if…elif…elif…else быстро становятся громоздкими и трудночитаемыми. Для таких ситуаций существует оператор case.

Его структура:

case $переменная in
шаблон1)
команды1
;;
шаблон2|шаблон3)
команды2
;;
*)
команды_по_умолчанию
;;
esac

Каждый шаблон проверяется по порядку. Как только находится совпадение, выполняется соответствующий блок, после чего управление передаётся за пределы case. Двойная точка с запятой ;; — обязательный разделитель, завершающий блок.

Шаблоны используют упрощённое сопоставление с образцом, похожее на то, что применяется в *.txt при работе с файлами:

  • * — любая последовательность символов (включая пустую),
  • ? — любой один символ,
  • [abc] — один из перечисленных символов,
  • [a-z] — любой символ из диапазона.

Пример: распознавание типа архива и его распаковка.

#!/bin/bash

# Получаем имя файла — либо из аргумента, либо спрашиваем
if [ $# -eq 0 ]; then
read -p "Укажите файл архива: " archive
else
archive=$1
fi

# Проверяем существование файла
if [ ! -f "$archive" ]; then
echo "Ошибка: файл '$archive' не найден"
exit 1
fi

# Выполняем действие в зависимости от расширения
case "$archive" in
*.tar)
echo "Распаковка TAR-архива..."
tar -xf "$archive"
;;
*.tar.gz|*.tgz)
echo "Распаковка GZIP-сжатого TAR..."
tar -xzf "$archive"
;;
*.tar.bz2|*.tbz)
echo "Распаковка BZIP2-сжатого TAR..."
tar -xjf "$archive"
;;
*.zip)
echo "Распаковка ZIP-архива..."
unzip "$archive"
;;
*.rar)
echo "Распаковка RAR-архива..."
unrar x "$archive"
;;
*.7z)
echo "Распаковка 7z-архива..."
7z x "$archive"
;;
*)
echo "Неизвестный формат архива: $archive"
exit 1
;;
esac

Обратите внимание на несколько деталей:

  • Переменная $archive заключена в кавычки — это гарантирует корректную обработку имён с пробелами.
  • Шаблоны объединены через | — например, .tar.gz и .tgz обрабатываются одинаково.
  • Блок *) — «все остальные случаи» — обеспечивает обработку неизвестных форматов.
  • Перед case проводится валидация входных данных: сначала запрашивается файл, затем проверяется его существование. Это делает скрипт устойчивым к ошибкам пользователя.

Такой подход применяется повсеместно: выбор режима работы (start, stop, restart), обработка опций (-v, --verbose, -h, --help), маршрутизация по типу устройства, ОС или версии ПО.


Обработка ошибок — надёжность через контролируемое завершение

Unix-философия предполагает, что программы должны «молча» выполнять свою работу, если всё в порядке, и чётко сигнализировать о проблемах, когда что-то пошло не так. Этот принцип лежит в основе надёжных скриптов.

Ключевой механизм — код возврата (exit code). Любая команда в Unix возвращает целое число от 0 до 255 по завершении:

  • 0 — успех,
  • 1–255 — ошибка (конкретное значение часто несёт смысл: 1 — общая ошибка, 2 — неправильное использование, 126 — файл не исполняем, 127 — команда не найдена).

Скрипт может получить код возврата последней команды через переменную $?:

ls /nonexistent
echo "Код возврата: $?"

Вывод будет 2, потому что ls не смог найти директорию.

Но проверять $? после каждой команды неудобно. Гораздо эффективнее встроить проверку прямо в условие:

if ! mkdir /critical/data; then
echo "Не удалось создать директорию — проверьте права и место на диске"
exit 1
fi

Здесь оператор ! инвертирует результат: блок then выполняется, только если mkdir завершился с ошибкой.

Можно комбинировать команды через && и ||:

  • команда1 && команда2команда2 выполнится, только если команда1 успешна;
  • команда1 || команда2команда2 выполнится, только если команда1 завершилась с ошибкой.

Пример надёжной последовательности:

cd /project && make clean && make all && ./deploy.sh

Если хотя бы один этап провалится, оставшиеся не запустятся — это предотвращает выполнение «на авось».

Для более сложных сценариев применяется конструкция set -e. Если добавить её в начало скрипта:

#!/bin/bash
set -e

— то скрипт автоматически завершится с ошибкой при первой же неудачной команде (кроме команд в условиях if, циклах или после ||).

Это мощный инструмент для обеспечения отказоустойчивости — особенно в скриптах развёртывания или миграции, где частичное выполнение может оставить систему в несогласованном состоянии.

Также полезны другие опции:

  • set -u — завершает скрипт при попытке использовать неопределённую переменную (например, $undef вместо $defined);
  • set -o pipefail — делает весь конвейер (cmd1 | cmd2 | cmd3) неудачным, если любая команда в нём вернула ошибку (по умолчанию учитывается только код последней).

Пример безопасного скрипта с заголовком:

#!/bin/bash
set -euo pipefail

# Теперь любая ошибка, неопределённая переменная или сбой в пайпе — приведут к остановке

Это стандарт де-факто для профессиональных скриптов. Он превращает потенциально незаметные проблемы в явные сбои, которые можно диагностировать.


Функции — модульность внутри скрипта

Когда скрипт вырастает за пределы нескольких десятков строк, возникает потребность в повторном использовании кода. Вместо копирования одних и тех же блоков в разных местах применяются функции.

Функция — это именованный блок команд, который можно вызывать по имени, передавая ему аргументы.

Объявление:

имя_функции() {
команды
}

Вызов:

имя_функции арг1 арг2

Внутри функции аргументы доступны как $1, $2 и т.д., а $# показывает их количество — точно так же, как в основном скрипте.

Пример: функция для логирования с временной меткой.

log() {
local level=$1
local message=$2
local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
echo "[$timestamp] [$level] $message" >> /var/log/myscript.log
}

# Использование
log "INFO" "Начало обработки"
log "WARN" "Файл отсутствует, пропускаем"
log "ERROR" "Не удалось подключиться к базе"

Ключевое слово local ограничивает область видимости переменной внутри функции. Без него переменная станет глобальной и может случайно перезаписать значение с тем же именем в основном скрипте.

Функции могут возвращать значения через echo, а вызывающий код — перехватывать их с помощью командной подстановки $():

get_temp_dir() {
mktemp -d "/tmp/backup.XXXXXX"
}

workdir=$(get_temp_dir)
echo "Рабочая директория: $workdir"

Это позволяет строить скрипты как набор независимых, тестируемых компонентов. Можно, например, вынести в функции:

  • проверку зависимостей (check_tools),
  • создание резервной копии (backup_database),
  • отправку уведомления (notify_user),
  • очистку временных файлов (cleanup).

Такая структура упрощает чтение, поддержку и отладку. Даже если скрипт остаётся одним файлом, он перестаёт быть «сплошным потоком» и превращается в архитектурно осмысленную программу.


Практические примеры: скрипты, которые работают здесь и сейчас

Теория важна, но истинная ценность скриптов — в их применении. Рассмотрим несколько реальных задач, решаемых небольшими, но мощными сценариями.

1. Ежедневное резервное копирование домашней директории

#!/bin/bash
set -euo pipefail

# Настройки
USER_HOME="/home/timur"
BACKUP_ROOT="/backups"
DATE=$(date "+%Y-%m-%d")
BACKUP_NAME="home_backup_$DATE.tar.gz"
BACKUP_PATH="$BACKUP_ROOT/$BACKUP_NAME"

# Создаём директорию, если её нет
mkdir -p "$BACKUP_ROOT"

# Логируем начало
echo "[$(date)] Начало резервного копирования $USER_HOME$BACKUP_PATH"

# Создаём архив, исключая кэш и временные файлы
tar -czf "$BACKUP_PATH" \
--exclude="$USER_HOME/.cache" \
--exclude="$USER_HOME/.local/share/Trash" \
--exclude="$USER_HOME/Downloads/*.tmp" \
"$USER_HOME"

# Проверяем размер архива
size=$(stat -c%s "$BACKUP_PATH" 2>/dev/null || echo 0)
if [ "$size" -lt 1000 ]; then
echo "[$(date)] ОШИБКА: архив подозрительно мал ($size байт)"
rm -f "$BACKUP_PATH"
exit 1
fi

echo "[$(date)] Успешно создано: $BACKUP_PATH ($size байт)"
echo "[$(date)] Очистка старых резервных копий (>7 дней)"

# Удаляем копии старше 7 дней
find "$BACKUP_ROOT" -name "home_backup_*.tar.gz" -mtime +7 -delete

echo "[$(date)] Резервное копирование завершено"

Этот скрипт можно добавить в crontab:

0 2 * * * /home/timur/bin/daily-backup.sh

— и каждую ночь в 2:00 он будет создавать сжатую копию домашней директории, избегая мусора, проверяя целостность и удаляя устаревшие архивы.

2. Быстрый мониторинг доступности сервиса

#!/bin/bash

HOST="api.example.com"
PORT=443
TIMEOUT=5

echo "Проверка доступности $HOST:$PORT..."

if timeout "$TIMEOUT" bash -c "</dev/tcp/$HOST/$PORT" 2>/dev/null; then
echo "✓ Сервис доступен"
exit 0
else
echo "✗ Сервис недоступен"
# Можно добавить уведомление: отправку email, лог в syslog, вызов вебхука
logger -t "monitor" "Сбой: $HOST:$PORT недоступен"
exit 1
fi

Хитрость здесь — в использовании встроенного в Bash перенаправления /dev/tcp/host/port. Это не внешняя утилита, а встроенная возможность оболочки, позволяющая открыть TCP-соединение без nc, telnet или curl. Если соединение устанавливается — команда успешна.

3. Пакетная переименовка файлов — приведение к единому стилю

#!/bin/bash

# Пример: переименовать все .JPG в .jpg и заменить пробелы на подчёркивания
for file in *.{JPG,jpg,JPEG,jpeg}; do
# Пропускаем, если шаблон не совпал ни с чем
[ -e "$file" ] || continue

# Новое имя: нижний регистр, пробелы → _, множественные _ → один
newname=$(echo "$file" | tr ' ' '_' | tr '[:upper:]' '[:lower:]' | sed 's/__*/_/g')

if [ "$file" != "$newname" ]; then
echo "Переименовать: '$file' → '$newname'"
mv "$file" "$newname"
fi
done

Этот скрипт работает как «умный» rename: он не просто меняет расширение, а нормализует имя — что особенно полезно при подготовке медиафайлов к публикации, импорту в CMS или отправке в облако.


Границы применимости: когда скрипт — правильный выбор, а когда — нет

Shell-скрипты — не универсальный инструмент. Их сила проявляется в задачах, тесно связанных с операционной системой: запуск программ, управление файлами, сбор информации, координация других утилит. Там, где основная работа делается внешними командами (grep, awk, sed, find, curl, tar, ssh), а оболочка лишь связывает их в последовательность — скрипты незаменимы.

Они особенно эффективны, когда:

  • задача разовая или редкая, и нет смысла писать полноценное приложение;
  • требуется быстрое решение «здесь и сейчас» — например, восстановление после сбоя;
  • автоматизация должна работать в минимальных окружениях (даже в initramfs или rescue-системе);
  • интеграция идёт с другими Unix-утилитами через стандартные потоки (stdin/stdout/stderr);
  • важна прозрачность: любой системный администратор может открыть скрипт и понять, что он делает, без компиляции и отладчика.

Однако есть ситуации, где shell-скрипты теряют преимущество:

  • требуется сложная обработка текста с регулярными выражениями, разбором структур (JSON, XML) — здесь предпочтительнее awk, jq, или полноценные языки вроде Python;
  • нужны многопоточность или асинхронный ввод-вывод — Bash однопоточен по своей природе;
  • программа должна быть высокоуровневой, с графическим интерфейсом или веб-API — скрипты не предназначены для этого;
  • объём кода превышает 200–300 строк — читаемость и поддерживаемость резко падают;
  • критична производительность при большом числе итераций — каждый вызов внешней команды порождает новый процесс, что дороже внутренних операций в интерпретируемых языках.

В таких случаях скрипт может остаться «обёрткой» — точкой входа, которая вызывает более мощные инструменты. Например, установочный сценарий может быть написан на Bash, но внутри запускать Python-скрипт для конфигурации или сборки.

Хорошее правило: если 80 % работы делают внешние команды — пишите на shell; если 80 % — логика внутри — выбирайте другой язык.


Стиль и культура написания скриптов

Качественный скрипт — это не только рабочий, но и понятный, безопасный, сопровождаемый код. Даже если он написан для личного использования, хорошие практики экономят часы при возврате к нему через месяц.

1. Заголовок — паспорт скрипта

В начале файла — комментарий с описанием:

#!/bin/bash
# backup-home.sh
# Автор: Тагиров Тимур
# Дата: 2025-12-22
# Назначение: Создание сжатой резервной копии домашней директории
# Использование: ./backup-home.sh [путь_к_резерву]
# Зависимости: tar, gzip, find, stat

Это позволяет мгновенно понять, зачем существует файл, как его запускать и что ему нужно.

2. Явное указание интерпретатора и режима

Всегда указывайте #!/bin/bash, если используете Bash-специфичные конструкции ([[ ]], (( )), local). Избегайте #!/bin/sh, если только не пишете для максимальной переносимости (например, для init-скриптов в embedded-системах).

Добавьте в начало тело скрипта:

set -euo pipefail

— это стандарт обеспечения надёжности, принятый в профессиональных проектах (например, в kubernetes, docker, ansible).

3. Конфигурация в начале — параметризация

Выносите все настраиваемые значения в начало, как переменные:

BACKUP_DIR="/backups"
RETENTION_DAYS=7
LOG_FILE="/var/log/backup.log"

Это позволяет изменять поведение без поиска по всему коду. Для продвинутых сценариев — принимайте значения из переменных окружения с резервным значением:

BACKUP_DIR="${BACKUP_DIR:-/backups}"

Если переменная BACKUP_DIR задана в окружении — используется она; иначе — значение по умолчанию.

4. Комментарии — объясняйте почему, а не что

Команды mkdir, cp, grep и так понятны. Комментарий должен раскрывать намерение:

# Создаём временную директорию во избежание конфликтов при параллельных запусках
tmpdir=$(mktemp -d)

# Игнорируем ошибку, если файл уже удалён (например, другим процессом)
rm -f "$lockfile" 2>/dev/null || true

5. Безопасность ввода — никогда не доверяйте данным

Любой внешний ввод — аргументы, переменные окружения, вывод команд — потенциально опасен. Всегда:

  • заключайте переменные в кавычки: "$file", а не $file;
  • проверяйте существование файлов и директорий перед использованием;
  • избегайте eval, если нет крайней необходимости — он выполняет произвольный код;
  • не запускайте скрипты с sudo, если они не проверены — лучше выносить привилегированные действия в отдельные sudo-вызовы внутри.

6. Логирование — окно в работу скрипта

Вместо echo используйте функцию логирования, которая пишет в файл и, при желании, на экран:

log() {
local level=${1:-INFO}
local msg="$2"
local now=$(date "+%Y-%m-%d %H:%M:%S")
printf "[%s] [%s] %s\n" "$now" "$level" "$msg" | tee -a "$LOG_FILE"
}

Флаг -a у tee добавляет строки в конец файла, не перезаписывая его.


Практика: упражнения для закрепления

Теория усваивается через действие. Предлагаем несколько задач, возрастающей сложности. К каждой — подсказка, как проверить результат.

Упражнение 1. «Счётчик файлов»

Напишите скрипт count-by-ext.sh, который принимает расширение (например, log) и подсчитывает, сколько файлов с таким расширением находится в текущей директории и её поддиректориях.

Подсказка: используйте find . -type f -name "*.$1" | wc -l.
Проверка: создайте три .log-файла в разных подпапках — скрипт должен вернуть 3.

Упражнение 2. «Оповещение о диске»

Создайте скрипт disk-check.sh, который проверяет использование корневого раздела (/). Если свободного места меньше 10 %, выводит предупреждение и отправляет запись в системный журнал через logger.

Подсказка: df / | awk 'NR==2 {print $5}' | tr -d '%' — получает процент использования.
Проверка: запустите с df вручную и сравните цифры.

Упражнение 3. «Безопасное удаление»

Реализуйте safe-rm.sh, который вместо удаления перемещает файлы в директорию ~/.trash. Если файл с таким именем уже там есть — добавляет суффикс _1, _2 и т.д.

Подсказка: используйте цикл while [ -e "$target" ], увеличивая счётчик.
Проверка: удалите файл дважды подряд — в корзине должны быть две версии.

Упражнение 4. «Умное копирование»

Скрипт sync-dir.sh исходник назначение должен копировать только новые или изменённые файлы, сохраняя структуру. Если назначение — не директория, скрипт завершается с ошибкой.

Подсказка: rsync -av --dry-run для проверки, rsync -av для реального копирования.
Проверка: после первого запуска измените один файл и запустите снова — скопироваться должен только он.

Эти упражнения охватывают аргументы, проверку условий, работу с файлами, взаимодействие с системой — всё, что мы изучили.


Итог: 10 принципов хорошего Unix-скрипта

  1. Явность превыше краткости. Лучше длинное, но понятное имя переменной (backup_directory), чем bd.

  2. Проверяй — не верь. Всегда проверяй существование файлов, права, количество аргументов.

  3. Кавычки — твой щит. "$variable" — правило по умолчанию.

  4. set -euo pipefail — стандарт надёжности. Включай в начало каждого серьёзного скрипта.

  5. Функции — модули мышления. Выноси повторяющуюся логику (логирование, проверка, очистка).

  6. Код возврата — язык общения. Уважай его: возвращай 0 при успехе, ненулевое — при ошибке.

  7. Комментарии объясняют намерение. Не «что делает mkdir», а «почему создаём именно эту директорию».

  8. Избегай eval и динамического кода. Безопасность важнее гибкости.

  9. Тестируй на крайних случаях. Пустые аргументы, имена с пробелами, отсутствие программ.

  10. Скрипт — документ. Заголовок, использование, зависимости — должны быть в файле.